package visam.core;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Vector;

import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;

public class Buffer {
    private int             gapsize       = 0;
    private char[]          buffer;
    private int             point         = 0;
    private int             gapstart      = 0;
    private int             gapend        = 0;
    private int             column;
    private int             line;
    private boolean         visible;
    private Vector<Integer> newlineMarkers;                           // Marks the new lines in the buffer with the gaps in order to draw them properly;
    private int             offset;
    private int             noOfLines;
    private Rectangle       dimensions    = new Rectangle(0, 0, 0, 0);
    private boolean         colonMode     = false;
    private boolean         editMode      = false;
    
    public static final int LEADING_EDGE  = 0;
    public static final int TRAILING_EDGE = 1;
    
    private BlinkTimer      bt;
    
    private String          filename;
    
    public Buffer(int gapSize) {
        initBuffer(null, gapSize);
    }
    
    public Buffer(String fname, int size) throws FileNotFoundException, IOException {
        File file = new File(fname);
        filename = fname;
        gapsize = size;
        int initFileLen = (int)file.length();
        buffer = new char[initFileLen + gapsize];
        FileReader fr = new FileReader(file);
        gapstart = fr.read(buffer);
        gapend = buffer.length;
        gapsize = gapend - gapstart;
        findNewLines();
        bt = new BlinkTimer();
        Thread t = new Thread(bt);
        t.start();
    }
    
    public int getBufferSize() {
        return buffer.length - (gapend - gapstart);
    }
    
    private boolean movePoint(int offset) {
        point = offset;
        
        return true;
    }
    
    public void moveGapToPoint() {
        int length = gapend - gapstart;
        if (point == gapstart) return;
        
        if (point == gapend) {
            point = gapstart;
            return;
        }
        
        if (point < gapstart) {
            System.arraycopy(buffer, point, buffer, point - (gapend - gapstart), gapstart - point);
            gapend -= gapstart - point;
            gapstart = point;
        }
        else {
            System.arraycopy(buffer, gapend, buffer, gapstart, (gapend - gapstart));
            gapstart += point - gapend;
            gapend = point;
            point = gapstart;
        }
    }
    
    public long pointOffset() {
        return buffer.length - point;
    }
    
    public char getCurrentChar() {
        if (point == gapstart) {
            point = gapend;
        }
        
        return buffer[(int)point];
    }
    
    public char getPreviousChar() {
        if (point == gapend) {
            point = gapstart;
        }
        return buffer[--point];
    }
    
    public void replaceChar(char c) {
        if (point == gapstart) {
            point = gapend;
        }
        
        if ((int)point == (buffer.length - 1)) {
            expandBuffer(1);
        }
        buffer[point] = c;
    }
    
    public char getNextChar() {
        if (point == gapstart) {
            point = gapend;
            return buffer[point];
        }
        return buffer[++point];
    }
    
    public void putChar(char c) {
        insert(c);
        point++;
    }
    
    public void delete(long size) {
        if (point != gapstart) {
            moveGapToPoint();
        }
        gapend += size;
    }
    
    /**
     * Delete the next character with regards to the point.
     * Store it in the undo list so that it can be regained later
     */
    public void deleteNext(int cnt) {
        gapend += cnt;
        resetGap();
    }
    
    public void insert(char c) {
        if ((gapend - gapstart) == 0) expandBuffer(gapsize);
        buffer[gapstart++] = c;
        point = gapstart;
        resetGap();
        if (c == '\n') findNewLines();
    }
    
    public void moveLineUp(int count) {
        int pos = findBOL(point);
        int bol = findBOL(point);
        for (int i = 0; i < count; i++) {
            bol = findBOL(pos - 2);
            pos = bol;
        }
        int nextline = findEOL(bol);
        if (column + bol > nextline) {
            column = nextline - 1 - bol;
        }
        
        setPoint(bol + column);
        visible = true;
    }
    
    public void setPos(int line, int column) {
        int diff = this.line - offset;
        this.line = line;
        this.column = column;
        this.offset = this.line - offset;
    }
    
    public void moveLineDown(int count) {
        int pos = point;
        int newline = point;
        for (int i = 0; i < count; i++) {
            newline = findEOL(pos);
            pos = newline + 1;
        }
        int nextline = findEOL(pos);
        if (newline + column > nextline) {
            column = nextline - 1 - newline;
        }
        
        setPoint(newline + 1 + column);
        visible = true;
    }
    
    public void moveCharRight(int count) {
        if (gapend + count <= buffer.length) {
            int newpos;
            int nextnl = findEOL(point);
            int prevnl = findBOL(point);
            int newColumn = column + count;
            System.out.print("[bol:" + prevnl + " eol:" + nextnl + " nc:" + newColumn);
            if (prevnl + newColumn >= nextnl) {
                newpos = nextnl + column;
            }
            else {
                newpos = point + count;
            }
            System.out.println("=> [col:" + column + " np:" + newpos);
            setPoint(newpos);
            visible = true;
        }
    }
    
    protected int findEOL(int start) {
        boolean compensate = false;
        
        if (start >= gapstart) {
            start += (gapend - gapstart);
            compensate = true;
        }
        
        int pos = start;
        
        do {
            if (pos == gapstart) {
                compensate = true;
                pos = gapend;
            }
            // else
            pos += 1;
        }
        while (pos < buffer.length && buffer[pos] != '\n');
        return pos - (compensate ? gapend - gapstart : 0);
    }
    
    protected int getOffset() {
        return offset;
    }
    
    protected int getLine() {
        return line;
    }
    
    protected int getMaxLineCount() {
        return newlineMarkers.size() - 1;
    }
    
    protected int getColumn() {
        return column;
    }
    
    protected int findBOL(int start) {
        if (start <= 0) return 0;
        int pos = start;
        do {
            if (pos > 0)
                pos = pos - 1;
            else
                break;
        }
        while (pos > 0 && buffer[pos] != '\n');
        return buffer[pos] == '\n' ? pos + 1 : pos;
    }
    
    public void moveCharLeft(int count) {
        if (point - count >= 0) {
            int newColumn = column - count;
            int newpos = point - count;
            int prevnl = findBOL(point);
            
            if (newColumn < 0) {
                // We have to move back over a line boundary
                if (newpos < prevnl) {
                    int ppnl = findBOL(prevnl - 1);
                    int c = prevnl + newColumn - ppnl;
                    newpos = ppnl + c;
                }
            }
            else {
                column -= count;
            }
            setPoint(newpos);
            visible = true;
        }
    }
    
    public boolean setPoint(int input) {
        if (input < 0) return false;
        if (input >= buffer.length - (gapend - gapstart)) return false;
        
        point = input;
        System.out.println("point: " + point);
        
        if (point < gapstart) {
            System.arraycopy(buffer, point, buffer, point + (gapend - gapstart), gapstart - point);
            gapend = point + (gapend - gapstart);
            gapstart = point;
        }
        else if (point > gapend) {
            System.arraycopy(buffer, gapend, buffer, gapstart, point - gapstart);
            int tgsz = gapend - gapstart;
            gapstart = point;
            gapend = gapstart + tgsz;
        }
        else {
            System.arraycopy(buffer, gapend, buffer, gapstart, point - gapstart);
            gapend = point + (gapend - gapstart);
            gapstart = point;
        }
        
        resetGap();
        findNewLines();
        
        int bol = findBOL(point);
        if (bol != 0) bol--;
        
        final int limit = newlineMarkers.size();
        for (int i = 0; i < limit; i++) {
            int foo = newlineMarkers.elementAt(i);
            if (foo == bol) {
                line = i;
                column = point - bol;
                if (i > offset + noOfLines) {
                    offset = i - noOfLines;
                }
                else if (i < offset) offset = i;
                break;
            }
        }
        
        return true;
    }
    
    public char[] getBuffer() {
        int bsz = getBufferSize();
        char[] rc = new char[bsz];
        System.arraycopy(buffer, 0, rc, 0, gapstart);
        System.arraycopy(buffer, gapend, rc, gapstart, bsz - gapstart);
        return rc;
    }
    
    public char[] getBuffer(int start, int end) {
        int bsz = end - start + 1;
        char[] rc = new char[bsz];
        System.arraycopy(buffer, start, rc, 0, bsz);
        return rc;
    }
    
    public int saveBufferToFile(FileOutputStream fos, long bytes) {
        return -1;
    }
    
    public int getGapSize() {
        return gapend - gapstart;
    }
    
    private void initBuffer(String fname, int gapSize) {
        this.gapsize = gapSize;
        int fileSize = 0;
        FileReader reader = null;
        char[] tmp;
        if (fname != null) {
            File file = new File(fname);
            fileSize = (int)file.length();
            buffer = new char[fileSize + gapSize];
            tmp = new char[fileSize];
            try {
                reader = new FileReader(file);
            }
            catch (FileNotFoundException fnfe) {
                fnfe.printStackTrace();
            }
            
            try {
                reader.read(buffer);
            }
            catch (IOException ioe) {
                ioe.printStackTrace();
            }
            
            gapstart = buffer.length;
            gapend = gapstart + gapSize;
            
            Thread t = new Thread(bt);
            t.run();
        }
    }
    
    protected void expandGap(int size) {
        if (size > gapsize) {
            size += gapsize;
            expandBuffer(size);
            System.arraycopy(buffer, gapend - size, buffer, gapend, buffer.length - gapend);
            gapend += size;
        }
    }
    
    private void resetGap() {
        for (int i = gapstart; i < gapend; i++) {
            buffer[i] = 0;
        }
    }
    
    public void findNewLines() {
        newlineMarkers = new Vector<Integer>();
        final int length = buffer.length;
        newlineMarkers.add(0);
        for (int i = 0; i < length; i++) {
            if (buffer[i] == '\n') {
                newlineMarkers.add(new Integer(i));
            }
        }
    }
    
    public void extendGap(int start, int end) {
        if (start < gapstart)
            gapstart = start;
        else
            gapend += end - start;
        resetGap();
    }
    
    public void extendGap(int extpos) {
        if (extpos < gapstart)
            gapstart = extpos;
        else
            gapend += extpos - gapstart;
        resetGap();
    }
    
    /**
     * Draw the contents of the buffer on the screen
     * 
     * @param gc
     *            - handle to the GC for drawing to the screen
     * @param rect
     *            - the dimensions available ror drawing
     */
    public void draw(GC gc, Rectangle rect) {
        int from = 0;
        FontMetrics fm = gc.getFontMetrics();
        final int limit = (int)newlineMarkers.size();
        int lc = 0;
        int to;
        String line;
        int count;
        
        if (!dimensions.equals(rect)) {
            dimensions.x = rect.x;
            dimensions.y = rect.y;
            dimensions.width = rect.width;
            dimensions.height = rect.height;
        }
        
        // refresh the ln brk tbl
        findNewLines();
        noOfLines = (int)Math.floor(rect.height / fm.getHeight());
        from = newlineMarkers.elementAt(offset) + (offset > 0 ? 1 : 0);
        
        // Draw the string onto the canvas, up until the gap starts.
        for (int i = offset + 1; i < limit; i++) {
            to = (int)newlineMarkers.elementAt(i);
            if (gapstart >= from && gapstart <= to) {
                line = new String(buffer, from, gapstart - from);
                Point p = gc.textExtent(line);
                line += new String(buffer, gapend, to - gapend);
                gc.drawText(line, 3, lc * fm.getHeight());
                
                if (visible && !colonMode()) {
                    gc.drawRectangle(3 + p.x, lc * fm.getHeight(), editMode() ? 1 : 5, fm.getHeight());
                }
            }
            else {
                count = to - from;
                line = new String(buffer, from, count < 0 ? 0 : count);
                gc.drawText(line, 3, fm.getHeight() * lc);
            }
            from = to + 1;
            lc += 1;
            if (lc >= noOfLines) break;
        }
    }
    
    public void setEditMode(boolean input) {
        editMode = input;
    }
    
    public boolean editMode() {
        return editMode == true;
    }
    
    public void setColonMode(boolean input) {
        colonMode = input;
    }
    
    public boolean colonMode() {
        return this.colonMode == true;
    }
    
    public void printBuffer() {
        char[] output = new char[buffer.length - (gapend - gapstart)];
        findNewLines();
        int nlcnt = 0;
        int from = 0;
        int to;
        String line;
        int count;
        for (Integer i : newlineMarkers) {
            to = (int)i;
            count = to - from;
            line = new String(buffer, from, count < 0 ? 0 : count);
            System.out.println(line);
            from = to + 1;
        }
        count = buffer.length - from;
        
        line = new String(buffer, from, count < 0 ? 0 : count);
        System.out.println(line);
    }
    
    /**
     * Insert the string into the gapbuffer and change the associated parameters accordingly.
     * 
     * @param string
     */
    public void insert(String string) {
        insert(string.toCharArray());
    }
    
    /**
     * Insert the array of characters in to the buffer.
     * 
     * @param carray
     *            - array of chars.
     */
    public void insert(char[] carray) {
        for (int i = 0; i < carray.length; i++) {
            insert(carray[i]);
        }
    }
    
    /**
     * Delete the previous character with regards to the point.
     * Store it in the undolist or the current Edit instance if it is deemed possible.
     */
    public void deletePrevious(int cnt) {
        gapstart -= cnt;
        point = gapstart;
        resetGap();
    }
    
    private int findChar(char ch) {
        int i = gapend;
        while (buffer[i] != ch && i > buffer.length) {
            i++;
        }
        return i - (gapend - gapstart);
    }
    
    private int findCharReversed(char ch) {
        int pos = point - 2;
        while (buffer[pos] != ch && pos >= 0) {
            pos -= 1;
        }
        return pos;
    }
    
    public int findPreviousEmptyLine() {
        if (point <= 0) return 0;
        int i = findBOL(point);
        while (getChar(i > 2 ? i - 2 : 0) != '\n') {
            if (i == 0) break;
            i = findBOL(i - 1);
        }
        return i;
    }
    
    public char getChar(int index) {
        if (index < gapstart) return buffer[index];
        return buffer[index + (gapend - gapstart)];
    }
    
    public char[] getText(int start, int end) {
        char[] rc = new char[end - start];
        char[] tmp = getBuffer();
        System.arraycopy(tmp, start, rc, 0, end - start);
        return rc;
    }
    
    public int reversedSkipOverWord(int start, int edge) {
        int pos = start;
        while (pos >= 0) {
            if (edge == LEADING_EDGE) {
                if (isWhitespace(buffer[pos]) && !isWhitespace(buffer[pos + 1])) break;
            }
            else if (edge == TRAILING_EDGE) {
                if (!isWhitespace(buffer[pos]) && isWhitespace(buffer[pos + 1])) break;
            }
            pos--;
        }
        return pos;
    }
    
    private boolean isWhitespace(char c) {
        return c == ' ' || c == '\t' || c == '\n';
    }
    
    public void expandBuffer(int size) {
        int oldlen = getBufferSize();;
        char[] tmp = new char[oldlen + size];
        try {
            System.arraycopy(buffer, 0, tmp, 0, gapstart);
            System.arraycopy(buffer, gapend, tmp, gapend + size, oldlen - gapstart);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        gapend = gapstart + size;
        buffer = tmp;
    }
    
    public void replaceText(final int start, final int end, final char[] text) {
        String tmp = new String(text, 0, text.length);
        System.out.println("replaceText: start: " + start + ", end " + end + ", text:" + tmp);
        char[] cb = getBuffer();
        int cblen = cb.length;
        int tlen = text.length;
        int nsz = cblen + tlen - (end - start);
        char[] copy = new char[nsz];
        System.arraycopy(cb, 0, copy, 0, start); // copy the first part of the text up until when the undo should start
        System.arraycopy(text, 0, copy, start, text.length); // copy over the text that is to be undone
        System.arraycopy(cb, end, copy, start + tlen, cblen - end); // copy the rest of the text from the original
        reinsertBuffer(copy);
    }
    
    public void reinsertBuffer(char[] incoming) {
        int ilen = incoming.length;
        int blen = buffer.length;
        buffer = new char[ilen + gapend - gapstart];
        System.arraycopy(incoming, 0, buffer, 0, gapstart);
        System.arraycopy(incoming, gapstart, buffer, gapend, incoming.length - gapstart);
        resetGap();
    }
    
    private class BlinkTimer implements Runnable {
        
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                try {
                    Thread.sleep(500);
                }
                catch (Exception e) {
                    // 
                }
                visible = !visible;
            }
        }
        
    }
    
    public void saveAs(String fname) throws IOException, NullPointerException {
        if (fname == null) throw new NullPointerException();
        
        File file = new File(filename == null ? fname : (filename.equals(fname) ? filename : fname));
        if (!file.exists()) file.createNewFile();
        char[] output = getBuffer();
        FileWriter fw = new FileWriter(file);
        fw.write(output);
        fw.close();
    }
    
    public void save() throws IOException {
        if (filename == null) throw new NullPointerException();
        File file = new File(filename);
        char[] output = getBuffer();
        FileWriter fw = new FileWriter(file);
        fw.write(output);
        fw.close();
        
    }
    
    public void open(String filename) throws IOException {
        File file = new File(filename);
        this.filename = filename;
        buffer = new char[(int)file.length() + gapsize];
        gapstart = (int)file.length();
        gapend = gapstart + gapsize;
        FileReader fr = new FileReader(file);
        fr.read(buffer);
        findNewLines();
        setPoint(0);
    }
    
    public int skipOverWord(int start, int edge) {
        int pos = gapend + (start - gapstart);
        
        if (edge == LEADING_EDGE && isWhitespace(buffer[gapstart]) && !isWhitespace(buffer[pos])) pos++;
        
        while (pos < buffer.length) {
            if (edge == LEADING_EDGE) {
                if (isWhitespace(buffer[pos - 1]) && !isWhitespace(buffer[pos])) break;
            }
            else if (edge == TRAILING_EDGE) {
                if (!isWhitespace(buffer[pos - 1]) && buffer[pos - 1] != 0 && isWhitespace(buffer[pos])) break;
            }
            pos++;
        }
        return pos - (gapend - gapstart);
    }
    
    public int getPoint() {
        return point;
    }
    
    public String getFileName() {
        return filename;
    }
}
